/* ModelSavyController.m created by cfeder on Tue 01-Nov-1994 */

/*
   Interesting master-detail relationships:
   M     Detail   Insert rule              Delete                 Types
   PK -> PK       D->PK := M->PK           delete D               to-one
   FK -> PK       M->FK := D->PK           M->FK=NULL             to-one
   PK -> FK       D->FK := M->PK           Delete D               to-many
   Many to many   create link obj          Delete link            to-many
*/
#import "SavvyControllerDelegate.h"
#import "RelationshipKeySetter.h"
#import "ResetRelationships.h"
#import "KeyGenerator.h"
#import "ValueForKey.h"


@interface EODetailDatabaseDataSource (databaseChannel)
- (EODatabaseChannel *)databaseChannel;
@end

@implementation EODetailDatabaseDataSource (databaseChannel)
- (EODatabaseChannel *)databaseChannel
{
    return [[self masterDataSource] databaseChannel];
}
@end



@implementation SavvyControllerDelegate

- (EORelationship *)detailRelationshipForController:controller;
{
    // Look up
    id dataSource = [controller dataSource];
    if ([dataSource respondsToSelector:@selector(masterObject)]) {
        EOEntity *entity = [dataSource entity];
        id masterObject = [dataSource masterObject];
        EOEntity *masterEntity = [[entity model] entityForObject:masterObject];
        return [masterEntity relationshipNamed:[dataSource detailKey]];        
    }
    return nil;
}

- (void)updateDetailObjectsForController:(EOController *)controller
{
    EORelationship *relationship = [self detailRelationshipForController:controller];
    if (relationship) {
        // if this is a detail controller, then we may have to update
        // the relationship property of the master EO, since we
        // may have supressed insert and delete operations bound
        // for the detailDataSource
        id masterObject = [(id)[controller dataSource] masterObject];
        NSArray *objects = [controller allObjects];
        if ([relationship isToMany]) {
            objects = [[[NSArray alloc] initWithArray:objects] autorelease];
        } else {
            objects = [objects count] ? [objects objectAtIndex:0] : [EONull null];
        }
        [masterObject takeValue:objects forKey:[relationship name]];
    }
}



// EOController delegation methods

- (BOOL)controller:(EOController *)controller willInsertObject:object atIndex: (unsigned)newIndex;
{
    // Handles primary key assignment for newly inserted objects.
    EOEntity *entity = [(id)[controller dataSource] entity];
    [object assignPrimaryKeyIfNotAlreadyPresentForEntity:entity];

    return YES;
}

// Handles insertions into a detail controller.  The tricky part here is
// that if we assign a new department in the detail controller for
// an employee, we don't want to actually add the department to the
// database (it's already there).  Instead, we want to update the foreign
// key in the master object to point at the new department.
// Similarly if we insert a project in the toProjects (many-to-many) detail
// controller, we don't want to insert the project, but rather a "link"
// object in the intermediate table emp_projects that joins employee and
// project.
- (EODataSourceOperationDelegateResponse)controller:(EOController *)controller
    willInsertObject:object
    inDataSource:dataSource;
{
    EODataSourceOperationDelegateResponse response = EOPerformDataSourceOperation;
    EORelationship *relationship = [self detailRelationshipForController:controller];

    // This stuff only works if our dataSource is an EODetailDatabaseDataSource.
    // Otherwise we just pass the operation through
    if (relationship) {
        id masterObject = [dataSource masterObject];
        id modifiedObject;
        EODatabaseDataSource *masterDataSource =  [dataSource masterDataSource];

        // Invoke the magic of the ModelRelExtensions to figure out what to
        // do for this relationship
        modifiedObject = [relationship updateKeysForSourceObject:masterObject destinationObject:object];

        if (modifiedObject == object) {
            modifiedObject = nil;
        } else if (modifiedObject == masterObject) {
            // we need to update this one as well
            NSLog(@"Updating master object '%@'", masterObject);
            if (![masterDataSource updateObject:masterObject])
                response = EORollbackDataSourceOperation;  // put up error?
        } else {
            // link object that we need to insert
            NSLog(@"Inserting link object '%@'", modifiedObject);
            if (![masterDataSource insertObject:modifiedObject])
                response = EORollbackDataSourceOperation;  // put up error?
        }

        if (modifiedObject) {
            // do we really need to insert this object?  Or did the insert just indicate
            // a reference to an existing object?
            EODatabaseContext *context = [[dataSource databaseChannel] databaseContext];
            if ([context snapshotForObject:object])
                response = EODiscardDataSourceOperation;
        }
    }    
    if (response == EOPerformDataSourceOperation)
        NSLog(@"Allowing insert of object '%@'", object);
    else {
        NSLog(@"Suppressing insert of object: '%@'", object);
    }
    return response;
}

- (EODataSourceOperationDelegateResponse)controller:(EOController *)controller
    willDeleteObject:object
    inDataSource:dataSource;
{
    /*Deletion of detail record may dictate one of the following...
       M     Detail   Insert rule              Delete                 Types
       PK -> PK       D->PK := M->PK           delete D               to-one
       FK -> PK       M->FK := D->PK           M->FK=NULL             to-one
       PK -> FK       D->FK := M->PK           Delete D               to-many
       Many to many   create link obj          Delete link            to-many
    */
    EODataSourceOperationDelegateResponse response = EOPerformDataSourceOperation;
    EORelationship *relationship = [self detailRelationshipForController:controller];

    // This stuff only works if our dataSource is an EODetailDatabaseDataSource.
    // Otherwise we just pass the operation through
    if (relationship) {
        EOEntity *entity = [relationship destinationEntity];
        id masterObject = [dataSource masterObject];
        id modifiedObject;

        // Invoke the magic of the ModelRelExtensions to figure out what to
        // do for this relationship
        modifiedObject = [relationship updateKeysForDeleteOfDestinationObject:object fromSourceObject:masterObject];

        if (modifiedObject == object) {
            // just go ahead and delete this one
        } else if (modifiedObject == masterObject) {
            // we need to update the master to reflect the delete
            // and leave this object alone
            response = EODiscardDataSourceOperation;
            NSLog(@"Updating master object '%@'", masterObject);
            if (![dataSource updateObject:masterObject])
                response = EORollbackDataSourceOperation;  // put up error?
        } else {
            // we need to delete the link object
            // and leave this object alone
            EODatabaseContext *context = [[dataSource databaseChannel] databaseContext];
            EODatabase *database = [context database];
            EOEntity *linkEntity = [[entity model] entityForObject:modifiedObject];
            NSDictionary *pkDict;
            id registeredObject;

            response = EODiscardDataSourceOperation; // leave destination object alone

            // to delete the object we need to make sure that it's in the uniquing
            // tables.  Check if it's already there, and if not, record this one
            pkDict = [modifiedObject valuesForKeys:[linkEntity primaryKeyAttributeNames]];
            registeredObject = [context objectForPrimaryKey:pkDict entity:linkEntity];
            if (!registeredObject) {
                [database recordObject:modifiedObject primaryKey:pkDict
                             snapshot:pkDict];
                registeredObject = modifiedObject;
            }
            NSLog(@"Deleting link object '%@'", registeredObject);
            if (![[dataSource masterDataSource] deleteObject:registeredObject])
                response = EORollbackDataSourceOperation;  // put up error?
        }
    }
    if (response == EOPerformDataSourceOperation)
        NSLog(@"Allowing delete of object '%@'", object);
    else {
        NSLog(@"Suppressing delete of object: '%@'", object);
    }
    return response;
}

- (void)controllerDidSaveToDataSource:(EOController *)controller
{
    [self updateDetailObjectsForController:controller];
}

@end


// This is probably evil.
// This code checks modifications to foreign key properties in an EO, and
// reassigns any corresponding relationship pointers using methods in
// ResetRelationships.h.
//
// THIS CODE IS NOT ACTUALLY USED IN THE EXAMPLE PROGRAM, but is here as
// an illustration.
@implementation SavvyControllerDelegate (ResetRelationshipsForChangedKeyProps)
static NSArray *changedKeys = nil;

- (NSDictionary *)controller:(EOController *)controller willSaveEdits: (NSDictionary *)edits toObject:object
{
    // update any related relationship properties (in case we update foreign keys)
    changedKeys = [edits allKeys];
    return edits;
}

- (void)controller:(EOController *)controller didSaveToObject:object
{
    EODatabaseDataSource *ds = (EODatabaseDataSource *)[controller dataSource];
    EOEntity *entity = [ds entity];

    // update relationships for key changes
    [[ds databaseChannel] updateRelationshipsInObject:object forModifiedAttributes:[entity attributesNamed:changedKeys]];

    // update keys for relationship changes
    {
        int i, count = [changedKeys count];
        for(i=0; i<count; i++) {
            NSString *key = [changedKeys objectAtIndex:i];
            EORelationship *relationship = [entity relationshipNamed:key];
            if (relationship) {
                [relationship updateKeysForSourceObject:object
                                      destinationObject:[(NSObject *)object valueForKey:key]];
            }
        }
    }
}
@end
